/**
 * Copyright © 2012, 2013 dr. ir. Jeroen M. Valk
 * 
 * This file is part of ComPosiX.
 * 
 * ComPosiX is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * ComPosiX is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * ComPosiX. If not, see <http://www.gnu.org/licenses/>.
 */

function Fifo() {
	function createBuffer(size) {
		var result = new Array();

		result.head = function() {
			if (this.length > 0) {
				return this[0];
			} else {
				return null;
			}
		};

		result.append = function(offset, array) {
			assert(offset < array.length);
			var space = size - this.length;
			if (size < 0 || array.length - offset <= space) {
				for ( var i = offset; i < array.length; ++i) {
					this.push(array[i]);
				}
				return 0;
			} else {
				for ( var i = 0; i < space; ++i) {
					this.push(array[offset + i]);
				}
				return array.length - space;
			}
		};

		return result;
	}

	var cont = null;
	var readbuf = null, writebuf = null, flushed = 0, closed = true;

	function assert(condition) {
		if (!condition) {
			throw new Error("Fifo: internal error");
		}
	}

	this.wait = function(continuation) {
		if (flushed > 0) {
			continuation(flushed);
		} else {
			assert(cont == null);
			cont = continuation;
		}
	};

	this.open = function(size) {
		if (typeof size != 'number') {
			throw new Error(
					'Fifo.open(size): must specify buffer size; use -1 for unlimited buffer');
		}
		if (closed) {
			if (flushed == 0) {
				writebuf = createBuffer(size);
				readbuf = createBuffer(size);
				closed = false;
			} else {
				throw new Error('Fifo.open(size): buffer not empty');
			}
		} else {
			throw new Error('Fifo.open(size): already open');
		}
	};

	this.send = function(msgv) {
		if (closed) {
			return 0;
		} else {
			if (msgv.length == 0) {
				throw new Error('Fifo.send(msgv): msgv must be non-empty');
			}
			if (!msgv.length) {
				throw new Error("Fifo.send(msgv): msgv must be an array");
			}
			var todo = writebuf.append(0, msgv);
			if (todo > 0) {
				this.flush();
				todo = writebuf.append(msgv.length - todo, msgv);
				if (todo > 0) {
					throw new Error('Fifo.send(msgv): buffer overflow by '
							+ todo);
				}
			}
			return msgv.length;
		}
	};

	this.read = function(n, continuation) {
		if (closed) {
			var result;
			if (flushed < n) {
				result = writebuf.splice(0, flushed);
				flushed = 0;
			} else {
				result = writebuf.splice(0, n);
				flushed -= n;
			}
			continuation(result);
		} else {
			if (flushed < n) {
				readbuf.push({
					n : n,
					cont : continuation
				});
			} else {
				flushed -= n;
				continuation(writebuf.splice(0, n));
			}
		}
	};

	this.size = function() {
		if (closed) {
			if (flushed > 0) {
				return flushed;
			} else {
				return -1;
			}
		} else {
			return flushed;
		}
	};

	this.peek = function(i) {
		if (i < flushed && i > 0) {
			return writebuf[i];
		} else {
			throw new Error('Fifo.peek(i): index out of range');
		}
	};

	this.flush = function() {
		if (closed) {
			return false;
		} else {
			var readv = new Array();
			var writev = new Array();
			while (readbuf.head() != null
					&& readbuf.head().n <= writebuf.length) {
				var aux = readbuf.shift();
				readv.push(aux);
				writev.push(writebuf.splice(0, aux.n));
			}
			flushed = writebuf.length;
			for ( var i = 0; i < readv.length; ++i) {
				readv[i].cont(writev[i]);
			}
			if (cont != null && (readv.length == 0 || flushed > 0)) {
				var continuation = cont;
				cont = null;
				continuation(flushed);
			}
			return true;
		}
	};

	this.close = function() {
		if (!closed) {
			closed = true;
			var readv = new Array();
			var writev = new Array();
			while (readbuf.head() != null
					&& readbuf.head().n <= writebuf.length) {
				var aux = readbuf.shift();
				readv.push(aux);
				writev.push(writebuf.splice(0, aux.n));
			}
			if (readbuf.head() != null) {
				var aux = readbuf.shift();
				assert(writebuf.length < aux.n);
				readv.push(aux);
				writev.push(writebuf.splice(0, writebuf.length));
				while (readbuf.head() != null) {
					aux = readbuf.shift();
					readv.push(aux);
					writev.push(new Array());
				}
				assert(writebuf.length == 0);
			}
			flushed = writebuf.length;
			for ( var i = 0; i < readv.length; ++i) {
				readv[i].cont(writev[i]);
			}
			if (cont != null) {
				var continuation = cont;
				cont = null;
				continuation(flushed);
			}
		}
	};

	this.status = function(logger) {
		logger.info(readbuf);
		logger.info(writebuf);
	};
}
